package cn.android666.nopreviewcamera

import android.annotation.SuppressLint
import android.media.MediaCodec
import android.media.MediaCodecInfo
import android.media.MediaCodecList
import android.media.MediaFormat
import android.os.Build
import timber.log.Timber
import java.nio.ByteBuffer

class H264Encoder(private val videoWidth: Int, private val videoHeight: Int) {
    private val mMediaCodec: MediaCodec
//    private var inputBufferIndex: Int = MediaCodec.INFO_TRY_AGAIN_LATER
    private var outputBufferIndex: Int = MediaCodec.INFO_TRY_AGAIN_LATER
    private lateinit var inputBuffer: ByteBuffer
    private lateinit var outputBuffer: ByteBuffer
    @Suppress("SpellCheckingInspection")
    private lateinit var h264Bytes: ByteArray
    @Suppress("SpellCheckingInspection")
    private lateinit var keyFrameDatas: ByteArray
    private var mBufferInfo = MediaCodec.BufferInfo()

    private var startTime = 0L
    private var receiveFrameCount = 0
    @Suppress("PrivatePropertyName")
    private val _1K = 1000
    @Suppress("PrivatePropertyName")
    private val _1M = _1K * 1000

    private var flags = 0
    /** 表示已经调用了close()方法 */
    private var calledCloseMethod = false
    /** 表示已经调用了releaseMediaCodec()方法 */
    private var calledReleaseMediaCodecMethod = false
    private var timeoutUs = 10_000L
    private var h264SaverThread: H264SaverThread? = null
    private lateinit var renderer: YuvToSurfaceRenderer

    init {
        if (Build.VERSION.SDK_INT == 30) {
            timeoutUs = 0L
        }


        val bitrate = getBitrate(videoWidth, videoHeight)
        Timber.i("width = ${videoWidth}, height = ${videoHeight}, bitrate = ${bitrate / 1000f / 1000f}M")

        H264Util.printSupportedH264Encoder()

        mMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
        // mMediaCodec = MediaCodec.createByCodecName("OMX.google.h264.encoder") // 指定使用软编码器
        // MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC) 这样获取的H264编码器为：OMX.qcom.video.encoder.avc
        // OMX.qcom.video.encoder.avc   是硬件编码器，支持NV12
        // OMX.google.h264.encoder      是软件编码器，支持NV12、I420

        // 当视频旋转90度或270度时，此处的宽高要调换位置
        val mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, videoWidth, videoHeight)

        /*
        球机的编码器支持的颜色格式：
        COLOR_FormatYUV420SemiPlanar（NV12）  十进制：21
        COLOR_FormatYUV420Planar（I420）      十进制：19
        上面两种是过时的，都推荐用：COLOR_FormatYUV420Flexible，明明是两种不同格式，不知道为什么推荐用的是同一个
        COLOR_FormatYUV420Flexible  十进制：2135033992，十六进制：7F420888
        COLOR_FormatSurface  十进制2130708361，十六进制： 7F000789
        */
//        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, if (NgnApplication.isBall()) MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible else MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar)
        //@Suppress("DEPRECATION")
        //mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar)
        // 当需要使用mMediaCodec.createInputSurface()时，则颜色格式要使用：COLOR_FormatSurface
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)

        // 检查是否支持surface输入
        checkSupportedSurface()

        // 注：不能使用静态码率，否则视频画面会有不停的间隔地出现微模糊效果。虽然使用了动态码率，但是码率值还是要设置的，如果不设置在调用mMediaCodec.configure()时会报异常
        // 动态码率：真实码率会是我们设置的码率的上下范围浮动,码率会根据视频画面变化而实时变化。
        mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR) // 设置码率为动态码率，默认也是动态码率
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate) // 码率（即比特率）, 官方Demo这里的1000即1kbps，1000_000即1mpbs
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 25)      // 帧速（25帧/秒）
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2) // I帧间隔（1帧/2秒），因为帧 1秒出25帧，2秒就出50帧，所以I帧间隔为2的话就是每50帧出一个关键帧
        //MediaFormat.KEY_ROTATION: 描述输出到output surface上所需的顺时针旋转的键。 仅当使用output surface配置编解码器时才使用此键。
        // 关联值为整数，代表度。支持的值是0、90、180或270。如果未指定，则旋转默认为0。
        //mediaFormat.setInteger(MediaFormat.KEY_ROTATION, 90)

        // 第二个参数用于显示解码器的视频内容，第三个参数为编解码器的解密参数，第四个参数为指定为编码器
        mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)

        val inputSurface = mMediaCodec.createInputSurface()
        renderer = YuvToSurfaceRenderer(inputSurface)
//        mYuvToH264Encoder = YuvToH264Encoder(inputSurface)
//        mYuvToH264Encoder?.startEncoding()

        h264SaverThread = H264SaverThread().also { it.start() }

        mMediaCodec.start()
        //Timber.e("codeName = ${mMediaCodec.codecInfo.name}")
        Timber.i("H264Encoder init完成")
    }

    private fun checkSupportedSurface() {
        val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
        for (codecInfo in codecList.codecInfos) {
            if (!codecInfo.isEncoder) continue

            // 遍历编解码器支持的所有 MIME 类型（如 video/avc）
            for (mimeType in codecInfo.supportedTypes) {
                try {
                    val caps = codecInfo.getCapabilitiesForType(mimeType)
                    // 检查颜色格式是否包含 COLOR_FormatSurface
                    val supportSurface = caps.colorFormats.any { format ->
                        format == MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
                    }
                    if (supportSurface) {
                        Timber.i("${codecInfo.name} 支持 Surface 输入")
                    }
                } catch (e: IllegalArgumentException) {
                    // 忽略不支持的类型
                }
            }
        }
    }

    /**
     * 把NV12格式的Yuv数据编码为H264数据，此方法执行完就是编码完一帧的数据
     */
    @SuppressLint("SwitchIntDef")
    @Synchronized
    fun encodeYuvToH264(@Suppress("SpellCheckingInspection") yuvDatas: ByteArray) {
        if (calledReleaseMediaCodecMethod) return // 如果已经释放了编码器，则不处理，因为多线程，所以有可能释放的时候还有数据扔进来编码的

        if (yuvDatas.isEmpty()) {
            // 如果没数据了就发送结束信号
            mMediaCodec.signalEndOfInputStream()
        } else {
            // 如果有数据就用OpenGL ES进行渲染
            val renderStartTime = System.nanoTime()
            renderer.renderFrame(yuvDatas, videoWidth, videoHeight)
            Timber.i("渲染提交所需要时间：${(System.nanoTime() - renderStartTime) / 1e6}ms")
        }

        // 渲染到Surface后，编码器会自动取出数据进行编码，我们只需要从编码器取出编码好的数据即可。

        // 把YUV数据放入MediaCodec中进行H264编码
        /*inputBufferIndex = mMediaCodec.dequeueInputBuffer(timeoutUs) // 如果10毫秒都等不到可用缓冲，则这一帧的yuv数据将丢掉。谷歌官方Demo也是用的这个值
        @Suppress("SpellCheckingInspection")
        if (inputBufferIndex >= 0) {
            inputBuffer = mMediaCodec.getInputBuffer(inputBufferIndex)!!
            inputBuffer.put(yuvDatas) // 官方Demo在调用put之前会先调用inputBuffer.clear()，实际上并不需要
            // if (!isRuning) flag = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
            // 参数presentationTimeUs：指定我们这一帧数据应该在什么时间显示，单位为微秒，System.nanoTime()得到是当前时间的表示的纳秒。1000纳秒等于1微秒
            // 谷歌官方Demo计算方式为： presentationTimeUs = frameIndex * 1000000(1秒对应的微秒值） / fps。frameIndex从0开始
            // 比如fps为10帧/秒，则第1帧应该在0.0秒时显示，第2帧应该在0.1秒时显示，第3帧应该在0.2秒时显示，当然这些都要换成参数需要的微秒单位
            // 我发现presentationTimeUs不需要从0开始，只要把接收到帧数据时的当前时间传给它即可
            mMediaCodec.queueInputBuffer(
                inputBufferIndex,
                0,
                yuvDatas.size,
                System.nanoTime() / 1000,
                flags // 官方Demo默认也是用0，结束时用MediaCodec.BUFFER_FLAG_END_OF_STREAM
            )
        }*/

        // 如果已经没有数据了，可以调用mMediaCodec.signalEndOfInputStream()来发送一个达到流结尾的信号，但是不调用也没有任何影响

        // 从MediaCodec中取出编好的H264数据并使用（如保存、发送）
        outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, timeoutUs)
        if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            // 注：这里拿到的MediaFormat才是有效的，才可以给MediaMuxer使用
            //CodecData.videoMediaFormat = mMediaCodec.getOutputFormat(outputBufferIndex)
            mediaFormat = mMediaCodec.outputFormat
        }

        Timber.i("outputBufferIndex = $outputBufferIndex")

        while (outputBufferIndex >= 0) {
            /* 索引为负数时有这些值：
                    MediaCodec.INFO_TRY_AGAIN_LATER：-1       // 尚无可用输出，稍后再试（即在mMediaCodec.dequeueOutputBuffer中指定了超时，而且这个超时时间到了还没有可用输出则会返回这个值，
                                                                 此时不应该再循环取缓冲输出,因为已经没有数据可取了，应该跳出循环，开始等待新数据的输入）
                    MediaCodec.INFO_OUTPUT_FORMAT_CHANGED：-2 // 应该在接收缓冲区之前发生（也就是还没有进行编码工作之前发生），并且应该只发生一次。输出格式已更改，后续数据将采用新格式。 getOutputFormat（）返回新格式。
                                                                请注意，您还可以使用新的getOutputFormat（int）方法来获取特定输出缓冲区的格式。这使您不必跟踪输出格式更改。
                    MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED：-3 // 表示输出的缓冲区已经改变了，需要使用getOutputBuffers()来获取新的缓冲区，
                                                                此外，此事件表示视频缩放模式可能已重置为默认值。而这个函数在Api21中已经弃用，
                                                                所以我们在使用新的方式获取缓冲数据时可以忽略这个返回值的情况。
                                                                现在我们是使用getOutputBuffer(index)来获取缓冲输出，而不是用getOutputBuffers()来获取缓冲区
             */
            outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex)!!

            when (mBufferInfo.flags) {
                0, MediaCodec.BUFFER_FLAG_KEY_FRAME -> { // 普通帧、关键帧
                    // 保存mp4时，不要在关键前面插入配置数据，否则会出现花屏

                    h264Bytes = ByteArray(mBufferInfo.size)
                    outputBuffer.get(h264Bytes)
                    Timber.i("得到编码后的H264数据，大小为：${mBufferInfo.size}")

                    var isKeyFrame = false

                    // 如果是关键帧，需要在关键帧前面加上配置数据后才能进行网络传输
                    if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME) {
                        keyFrameDatas = ByteArray(configOutputBytes.size + h264Bytes.size)
                        System.arraycopy(configOutputBytes, 0, keyFrameDatas, 0, configOutputBytes.size)
                        System.arraycopy(h264Bytes, 0, keyFrameDatas, configOutputBytes.size, h264Bytes.size)
                        h264Bytes = keyFrameDatas
                        isKeyFrame = true
                    }

                    h264SaverThread?.addYUVDatas(H264Data(h264Bytes, isKeyFrame))
                }
                MediaCodec.BUFFER_FLAG_CODEC_CONFIG -> { // 配置数据，这个是配置的数据，包含了视频大小、帧速等信息，这个数据大小为30byte
                    // 在输出真正的视频数据之前会先输出这个配置数据，而且只会输出一次。关于这个配置数据，其实在MediaFormat中包含有，
                    // 所以不需要把这个数据传MediaMuxer
                    // TODO 官方写法：mBufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0，不知道为什么不这样写
                    Timber.i("得到配置数据，大小为：${mBufferInfo.size}")
                    configOutputBytes = ByteArray(mBufferInfo.size)
                    outputBuffer.get(configOutputBytes)
                }
                MediaCodec.BUFFER_FLAG_END_OF_STREAM -> {
                    // TODO 官方写法：mBufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0
                    // TODO 实际打印mBufferInfo.flags的值就是4，与BUFFER_FLAG_END_OF_STREAM相等，搞清楚为什么官方要这么写
                    Timber.i("已经到达流的终点了, mBufferInfo.flags = ${mBufferInfo.flags}")
                    mMediaCodec.releaseOutputBuffer(outputBufferIndex, false)
                    break // 退出循环，无需再去获取编码的数据，肯定没有数据了。
                }
            }

            mMediaCodec.releaseOutputBuffer(outputBufferIndex, false) // 把缓存对象还给MediaCodec
            outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, timeoutUs)
        }

        /*receiveFrameCount++
        if (System.currentTimeMillis() - startTime >= 10_000) {
            Timber.e("H264Encoder每10秒编码${receiveFrameCount}帧")
            startTime = System.currentTimeMillis()
            receiveFrameCount = 0
        }*/
    }

    private fun getBitrate(videoWidth: Int, videoHeight: Int): Int = when {
        (videoWidth == 1920 && videoHeight == 1080) || (videoWidth == 1080 && videoHeight == 1920) -> _1M * 4
        (videoWidth == 1280 && videoHeight == 720) || (videoWidth == 720 && videoHeight == 1280) -> _1M * 2
        (videoWidth == 640 && videoHeight == 480) || (videoWidth == 480 && videoHeight == 640) ->  _1M
        else -> _1M * 1
    }

    @Synchronized
    fun close() {
        Timber.i("close start")
        if (calledCloseMethod) return // 预防关闭函数被调用两次
        calledCloseMethod = true
        // 在惠众的执法仪上，经常能发现这种方式在编码器中执行不到flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM的情况，所以还是直接释放比较稳妥一点
        flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM
        encodeYuvToH264(ByteArray(0)) // 如果close在主线程调用，那这里就是耗时操作，如何让close也在子线程呢？与编码同一个线程，这样就不用做同步了。
        releaseMediaCodec()
        Timber.i("close end")
    }

    private fun releaseMediaCodec() {
        Timber.i("releaseMediaCodec() start")
        calledReleaseMediaCodecMethod = true

        try {
            mMediaCodec.stop()
        } catch (e: Exception) {
            Timber.w(e, "，别慌，正常停止${H264Encoder::class.java.simpleName}时出现的异常！")
        }

        try {
            mMediaCodec.release()
        } catch (e: Exception) {
            Timber.w(e, "，别慌，正常释放${H264Encoder::class.java.simpleName}时出现的异常！")
        }

        h264SaverThread?.close()
        h264SaverThread = null

        renderer.release()
//        mYuvToH264Encoder?.stopEncoding()
        mediaFormat = null
        Timber.i("releaseMediaCodec() end")
    }

    companion object {
//        private val BIT_RATE: Int = 0 // videoWidth * videoHeight * 3 * 8 * FRAME_RATE / COMPRESS_RATIO;
//        private val COMPRESS_RATIO = 256
        var mediaFormat: MediaFormat? = null
        lateinit var configOutputBytes: ByteArray

        fun hasSPSAndPPS() = ::configOutputBytes.isInitialized
    }
}
